﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using DomainDriver.CommonUtilities.Reflectors;
using DomainDriver.DomainModeling.DomainModels;
using DomainDriver.DomainModeling.DomainObjects;
using DomainDriver.DomainModeling.Events;
using DomainDriver.DomainModeling.Repositories;
using DomainDriver.DomainModeling.Queries;

namespace DomainDriver.DomainModeling.Constraints
{
    public class IndexConstraint<KEY, KEYED_DOMAIN_OBJECT> : ConstraintBase
        where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>
    {
        public const int IndexStartValue = 1;

        private string m_IndexPropertyName;
        private List<PropertyInfo> m_IndexPropertyInfos;
        private Dictionary<string, List<PropertyInfo>> m_PropertyInfosUniqueOnByName;

        public IndexConstraint(IDomainModel domainModel, Expression<Func<KEYED_DOMAIN_OBJECT, KEY>> indexPropertyExpression, ICollection<Expression<Func<KEYED_DOMAIN_OBJECT, KEY>>> propertyExpressionsUniqueOn)
            : this(domainModel, ClassReflector.GetPropertyName<KEYED_DOMAIN_OBJECT, KEY>(indexPropertyExpression), ClassReflector.ConvertExpressionsToNames(propertyExpressionsUniqueOn))
        { }

        public IndexConstraint(IDomainModel domainModel, string indexPropertyName, ICollection<string> propertyNamesUniqueOn)
            : base(domainModel)
        {
            m_IndexPropertyInfos = ExtractConstrainedPropertyInfo<KEYED_DOMAIN_OBJECT>(indexPropertyName);
            if (m_IndexPropertyInfos == null)
            { throw new ApplicationException(); }
            m_IndexPropertyName = indexPropertyName;

            m_PropertyInfosUniqueOnByName = new Dictionary<string, List<PropertyInfo>>();
            foreach (string propertyNameUniqueOn in propertyNamesUniqueOn)
            {
                List<PropertyInfo> propertyInfos = ExtractConstrainedPropertyInfo<KEYED_DOMAIN_OBJECT>(propertyNameUniqueOn);
                if (propertyInfos == null)
                { throw new ApplicationException(); }
                m_PropertyInfosUniqueOnByName.Add(propertyNameUniqueOn, propertyInfos);
            }

            Repository.AddingDomainObject += new AddingDomainObjectDelegate<KEY, KEYED_DOMAIN_OBJECT>(Repository_AddingDomainObject);
            Repository.UpdatingDomainObject += new UpdatingDomainObjectDelegate<KEY, KEYED_DOMAIN_OBJECT>(Repository_UpdatingDomainObject);
        }

        public ICollection<string> GetPropertyNamesUniqueOn()
        {
            return new List<string>(m_PropertyInfosUniqueOnByName.Keys);
        }

        public int GetIndexValueForObject(KEYED_DOMAIN_OBJECT domainObject)
        {
            IQuery<KEYED_DOMAIN_OBJECT> query = GetUniqueOnQuery(domainObject);
            ICollection<KEYED_DOMAIN_OBJECT> queryResults = Repository.Read(query);

            Nullable<int> lastIndexValue = null;
            foreach (KEYED_DOMAIN_OBJECT matchingDomainObject in queryResults)
            {
                object indexValueAsObject = ClassReflector.GetNestedPropertyValue(matchingDomainObject, m_IndexPropertyInfos);
                int indexValue = (int)indexValueAsObject;

                if (!lastIndexValue.HasValue)
                { lastIndexValue = indexValue; }
                else
                {
                    if (indexValue > lastIndexValue)
                    { lastIndexValue = indexValue; }
                }
            }

            if (!lastIndexValue.HasValue)
            { return IndexStartValue; }

            return (lastIndexValue.Value + 1);
        }

        protected IRepository<KEY, KEYED_DOMAIN_OBJECT> Repository
        {
            get { return DomainModel.GetRepository<KEY, KEYED_DOMAIN_OBJECT>(); }
        }

        protected IQuery<KEYED_DOMAIN_OBJECT> GetUniqueOnQuery(KEYED_DOMAIN_OBJECT domainObject)
        {
            AndQuery<KEYED_DOMAIN_OBJECT> mainQuery = new AndQuery<KEYED_DOMAIN_OBJECT>();
            foreach (string propertyNameUniqueOn in m_PropertyInfosUniqueOnByName.Keys)
            {
                List<PropertyInfo> nestedPropertyInfoUniqueOn = m_PropertyInfosUniqueOnByName[propertyNameUniqueOn];
                object queryFieldValue = ClassReflector.GetNestedPropertyValue(domainObject, nestedPropertyInfoUniqueOn);
                EqualsQuery<KEYED_DOMAIN_OBJECT> nestedQuery = new EqualsQuery<KEYED_DOMAIN_OBJECT>(propertyNameUniqueOn, queryFieldValue);
                mainQuery.AddNestedQuery(nestedQuery);
            }
            return mainQuery;
        }

        protected void Repository_AddingDomainObject(IRepository<KEY, KEYED_DOMAIN_OBJECT> sender, AddDomainObjectEventArgs<KEY, KEYED_DOMAIN_OBJECT> args)
        {
            if (!Enabled)
            { return; }

            int nextIndexValue = GetIndexValueForObject(args.ObjectToAdd);
            object existingIndexValue = ClassReflector.GetNestedPropertyValue(args.ObjectToAdd, m_IndexPropertyInfos);

            if (!nextIndexValue.Equals(existingIndexValue))
            {
                ClassReflector.SetNestedPropertyValue(args.ObjectToAdd, m_IndexPropertyInfos, nextIndexValue);
            }
        }

        protected void Repository_UpdatingDomainObject(IRepository<KEY, KEYED_DOMAIN_OBJECT> sender, UpdateDomainObjectEventArgs<KEY, KEYED_DOMAIN_OBJECT> args)
        {
            if (!Enabled)
            { return; }

            object originalIndexValue = ClassReflector.GetNestedPropertyValue(args.OriginalObject, m_IndexPropertyInfos);
            object updatedIndexValue = ClassReflector.GetNestedPropertyValue(args.UpdatedObject, m_IndexPropertyInfos);

            if ((originalIndexValue == null) || (updatedIndexValue == null))
            { throw new ApplicationException(); }
            if (!originalIndexValue.Equals(updatedIndexValue))
            { throw new ApplicationException(); }
        }
    }
}